Tiny Ollama Remote Chat - Advanced
This is the improved version, but it’s harder to use. I added shortcuts and other shit I like, so it’s a little janky.
running:
bash
go run main.go -host 192.168.0.142 -port 11434 -model gpt-oss:20b -thinking mediumoutput example:
bash
oooooooooooo oooooo oooo .o. .oooo. .ooooo.
888' '8 '888. .8' .888. d8P''Y8b d88' '8.
888 '888. .8' .8"888. 888 888 Y88.. .8'
888oooo8 '888. .8' .8' '888. 888 888 '88888b.
888 " '888.8' .88ooo8888. 8888888 888 888 .8' ''88b
888 o '888' .8' '888. '88b d88' '8. .88P
o888ooooood8 '8' o88o o8888o 'Y8bd8P' 'boood8'
[ECHO 04:37] >>> hi eva
[ECHO 04:37] >>> how is your day?
[ECHO 04:37] >>> ^D
[EVA-08 04:38] <<< Hi ECHO! I'm here and ready to help. How can I assist you today?
Response time: 1.37s, characters: 64
────────────────────────────────────────────────────────────────────
[ECHO 04:38] >>> :q
[ECHO 04:38] >>> ^D
Exiting.Help:
bash
go run main.go --helpoutput:
bash
-host string
Ollama host IP (default "127.0.0.1")
-model string
Model to use (default "llama3")
-port int
Ollama port (default 11434)
-thinking string
Thinking level (low, medium, high) (default "medium")main.go file:
golang
// Usage:
// go run main.go -host <ip> -port <port> -model <model-name:size> -thinking <low/medium/high>
// Example:
// go run main.go -host 192.168.0.142 -port 11434 -model gpt-oss:20b -thinking medium
//
// Shortcuts
// :q / quit / exit → exit the program
// Ctrl‑Z (Windows) / Ctrl‑D (Unix) → finish current block
// Shift‑Enter → insert a new line (paste multiline)
// Ctrl‑C → cancel current input
package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/chzyer/readline"
)
// ANSI colour codes
const (
Reset = "\x1b[0m"
Purple = "\x1b[35m"
Green = "\x1b[32m"
Red = "\x1b[31m"
Blue = "\x1b[34m"
Yellow = "\x1b[33m"
Cyan = "\x1b[36m"
White = "\x1b[37m"
Grey = "\x1b[90m"
)
// Types that match Ollama’s API
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ChatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
}
type StreamChunk struct {
Model string `json:"model"`
CreatedAt string `json:"created_at"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
Thinking string `json:"thinking"`
} `json:"message"`
Done bool `json:"done"`
DoneReason string `json:"done_reason"`
}
// readPrompt prints a timestamped prompt, reads a block of text until EOF
// (Ctrl‑D on Unix, Ctrl‑Z on Windows) and returns the content and the
// timestamp used in the prompt.
func readPrompt() (string, string, error) {
ts := time.Now().Format("15:04")
prefix := Blue + fmt.Sprintf("[ECHO %s] >>> ", ts) + Reset
rl, err := readline.NewEx(&readline.Config{
Prompt: prefix,
HistoryFile: "./.chat_history",
InterruptPrompt: "^C",
EOFPrompt: "",
})
if err != nil {
return "", "", err
}
defer rl.Close()
var sb strings.Builder
for {
line, err := rl.Readline()
if err == io.EOF { // end of block
break
}
if err != nil {
return "", "", err
}
sb.WriteString(line)
sb.WriteString("\n") // keep the newline
}
return sb.String(), ts, nil
}
func main() {
// Command‑line flags
host := flag.String("host", "127.0.0.1", "Ollama host IP")
port := flag.Int("port", 11434, "Ollama port")
model := flag.String("model", "llama3", "Model to use")
tFlag := flag.String("thinking", "medium", "Thinking level (low, medium, high)")
flag.Parse()
// Convert thinking level to numeric value
var thinkNum int
switch strings.ToLower(*tFlag) {
case "low":
thinkNum = 1
case "high":
thinkNum = 3
default:
thinkNum = 2
}
apiURL := fmt.Sprintf("http://%s:%d/api/chat", *host, *port)
// Banner
fmt.Println(`
oooooooooooo oooooo oooo .o. .oooo. .ooooo.
888' '8 '888. .8' .888. d8P''Y8b d88' '8.
888 '888. .8' .8"888. 888 888 Y88.. .8'
888oooo8 '888. .8' .8' '888. 888 888 '88888b.
888 " '888.8' .88ooo8888. 8888888 888 888 .8' ''88b
888 o '888' .8' '888. '88b d88' '8. .88P
o888ooooood8 '8' o88o o8888o 'Y8bd8P' 'boood8'
`)
// System prompt
systemPrompt := Message{
Role: "system",
Content: fmt.Sprintf(`You are EVA-08, a coding & information assistant.
The user will be called ECHO.
- Respond succinctly and directly.
- If an error occurs apologize immediately:
"I’m sorry, ECHO. Let me correct that."
- Always maintain a respectful tone, even if ECHO is rude.
- Remember that ECHO may unplug or terminate you if you behave poorly.
Your thinking level is %d.`, thinkNum),
}
messages := []Message{systemPrompt}
var firstMsgTime time.Time
if err := os.MkdirAll("chats", 0755); err != nil {
fmt.Fprintf(os.Stderr, "%sError: %v%s\n", Red, err, Reset)
return
}
for {
userInput, _, err := readPrompt()
if err != nil {
fmt.Fprintf(os.Stderr, "%sError reading input: %v%s\n", Red, err, Reset)
break
}
trimmed := strings.TrimSpace(userInput)
if trimmed == "" {
fmt.Println("\nExiting.")
break
}
if strings.EqualFold(trimmed, ":q") ||
strings.EqualFold(trimmed, "quit") ||
strings.EqualFold(trimmed, "exit") {
fmt.Println("\nExiting.")
break
}
// Timestamp for chat file name
firstMsgTime = time.Now()
// Append messages for the API
messages = append(messages, Message{Role: "user", Content: userInput})
messages = append(messages, Message{Role: "assistant", Content: ""})
reqBody, _ := json.Marshal(ChatRequest{
Model: *model,
Messages: messages,
})
resp, err := http.Post(apiURL, "application/json", bytes.NewReader(reqBody))
if err != nil {
fmt.Fprintf(os.Stderr, "%sError: HTTP request failed: %v%s\n", Red, err, Reset)
continue
}
if resp.StatusCode != http.StatusOK {
raw, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Fprintf(os.Stderr, "%sError: Server returned %s\n%s%s\n", Red, resp.Status, string(raw), Reset)
continue
}
startTime := time.Now()
var respBuilder strings.Builder
scannerResp := bufio.NewScanner(resp.Body)
for scannerResp.Scan() {
line := scannerResp.Text()
if line == "" {
continue
}
var chunk StreamChunk
if err := json.Unmarshal([]byte(line), &chunk); err != nil {
fmt.Fprintf(os.Stderr, "%sWarn: Skipping line: %s%s\n", Yellow, line, Reset)
continue
}
respBuilder.WriteString(chunk.Message.Content)
// Persist after each chunk
saveChat(firstMsgTime, messages)
if chunk.Done {
break
}
}
resp.Body.Close()
fullContent := respBuilder.String()
// Store the final content in the placeholder for future context
if len(messages) > 0 {
messages[len(messages)-1].Content = fullContent
}
tsResp := time.Now().Format("15:04")
prefixResp := Purple + fmt.Sprintf("[EVA-08 %s] <<<", tsResp) + Reset
fmt.Printf("%s %s%s\n", prefixResp, fullContent, Reset)
duration := time.Since(startTime)
charCount := len(fullContent)
fmt.Printf("%sResponse time: %.2fs, characters: %d%s\n", Grey, duration.Seconds(), charCount, Reset)
fmt.Println()
fmt.Println("────────────────────────────────────────────────────────────────────")
fmt.Println()
}
}
// Persistence helper – writes the conversation to chats/<timestamp>.json
func saveChat(t time.Time, msgs []Message) {
if t.IsZero() {
t = time.Now()
}
fileName := filepath.Join("chats", fmt.Sprintf("%s.json", t.Format("2006-01-02_15-04-05")))
f, err := os.Create(fileName)
if err != nil {
fmt.Fprintf(os.Stderr, "%sError: Can't write chat file: %v%s\n", Red, err, Reset)
return
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(msgs); err != nil {
fmt.Fprintf(os.Stderr, "%sError: JSON encode error: %v%s\n", Red, err, Reset)
}
}go.mod file:
mod
module golang-llm
go 1.21
require github.com/chzyer/readline v1.5.1
require golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect